/****

    activequant - activestocks.eu

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

	
	contact  : contact@activestocks.eu
    homepage : http://www.activestocks.eu

****/
package org.activequant.data.util.converter;

import java.util.Calendar;
import java.util.Iterator;
import java.util.TimeZone;

import org.activequant.core.domainmodel.SeriesSpecification;
import org.activequant.core.domainmodel.data.Candle;
import org.activequant.core.domainmodel.data.TradeIndication;
import org.activequant.core.types.TimeFrame;
import org.activequant.core.types.TimeStamp;
import org.activequant.data.retrieval.ISeriesDataIteratorSource;
import org.activequant.data.util.ProtectTimeIntervalIterator;
import org.apache.log4j.Logger;

/**
 * Converts Candle series iterator source to TradeIndication series iterator source. 
 * Request to fetch TradeIndications is converted to a request to fetch Candles, 
 * then TradeIndications are generated from Candles using an external strategy.
 * <br/>
 * CandleToTradeIndicationIteratorSourceConverter implements ISeriesDataIteratorSource&lt;TradeIndication&gt;.<br/>
 * Holds the following associated variables:
 * <ul>
 * <li>candleSource(ISeriesDataIteratorSource&lt;Candle&gt;)</li>
 * <li>conversion(ICandleToTradeIndicationConversion)</li>
 * <li>timeFrame(TimeFrame)</li>
 * </ul>
 * <b>History:</b><br>
 *  - [09.30.2008] Created (Mike Kroutikov)<br>
 *
 *  @author Mike Kroutikov
 */
public class CandleToTradeIndicationIteratorSourceConverter implements ISeriesDataIteratorSource<TradeIndication> {
	
	private final Logger log = Logger.getLogger(getClass());
	/**
	 * private final ISeriesDataIteratorSource&lt;Candle&gt; candleSource;
	 */
	private final ISeriesDataIteratorSource<Candle> candleSource;
	/**
	 * private ICandleToTradeIndicationConversion conversion = new LinearCandleToTradeIndicationConversion();
	 */
	private ICandleToTradeIndicationConversion conversion = new LinearCandleToTradeIndicationConversion();
	/**
	 * private TimeFrame timeFrame = TimeFrame.day();
	 */
	private TimeFrame timeFrame = TimeFrame.day();
	/**
	 * constructs a CandleToTradeIndicationIteratorSourceConverter(implements ISeriesDataIteratorSource&lt;TradeIndication&gt;) using the given source(ISeriesDataIteratorSource&lt;Candle&gt;)
	 * its set its associated candleSource(ISeriesDataIteratorSource&lt;Candle&gt;)
	 * @param source
	 */
	public CandleToTradeIndicationIteratorSourceConverter(ISeriesDataIteratorSource<Candle> source) {
		this.candleSource = source;
	}
	/**
	 * returns an Iterable&lt;TradeIndication&gt; which iterates the TradeIndications obtained by converting fetched Candles for the given seriesSpecification(SeriesSpecification)
	 * during the associated timeFrame(TimeFrame) interval. The associated conversion(ICandleToTradeIndicationConversion) is used to convert each Candle to a List of TradeIndications.
	 */
	public Iterable<TradeIndication> fetch(SeriesSpecification seriesSpecification)
			throws Exception {
		// passed seriesSpecification is not required to have timeFrame set
		// (or may have time frame of TICK set), because we are requesting
		// trade indication series. But candle series source
		// requires time frame to be set correctly. Do it here.
		SeriesSpecification candleSpec = new SeriesSpecification(seriesSpecification);
		candleSpec.setTimeFrame(timeFrame);
		TimeStamp start = seriesSpecification.getStartTimeStamp();
		TimeStamp end   = seriesSpecification.getEndTimeStamp();
		
		/*
		 * Lets request wider interval, so that we are guaranteed not to miss
		 * the candle.
		 * 
		 *  For example, if end timestamp is 15:00, and we just pass it to the candle source, we
		 *  will miss daily candle that covers time 9:00-16:00 and comes with 16:00 timestamp.
		 *  Thus, we have to request a wider interval. How much wider? At least the duration of a candle.
		 *  I will make the margin twice larger that that just to cover the (unlikely) situation when
		 *  day has 25 hours (daylight saving goes into effect).
		 */
		long margin = inferMarginMillis(timeFrame) * 1000000L * 2;
		log.debug("using safety time margin of " + margin + " nanos");
		
		start = new TimeStamp(start.getNanoseconds() - margin);
		end = new TimeStamp(end.getNanoseconds() + margin);
		
		candleSpec.setStartTimeStamp(start);
		candleSpec.setEndTimeStamp(end);

		log.info("fetching candles: " + candleSpec);
		final Iterator<Candle> candles = candleSource.fetch(candleSpec).iterator();

		Iterator<TradeIndication> iterator = new Iterator<TradeIndication>() {
			/**
			 * private Iterator&lt;TradeIndication&gt; currentList; 
			 */
			private Iterator<TradeIndication> currentList; 
			
			{
				if(candles.hasNext() == false) {
					currentList = null;
				} else {
					currentList = conversion.convert(candles.next()).iterator();
				}
			}

			public boolean hasNext() {
				if(currentList == null) {
					return false;
				} else if(currentList.hasNext()) {
					return true;
				} else if(!candles.hasNext()) {
					return false;
				}

				currentList = conversion.convert(candles.next()).iterator();
				return currentList.hasNext();
			}

			public TradeIndication next() {
				if(!currentList.hasNext()) {
					return null;
				}
				
				return currentList.next();
			}

			public void remove() {
				throw new UnsupportedOperationException();
			}
			
		};

		/*
		 * Since we used for candles a wider interval that requested by user, we need to
		 * wrap iterator to filter out out-of-interval entities.  
		 */
		final Iterator<TradeIndication> wrappedIterator = new ProtectTimeIntervalIterator<TradeIndication>(
				iterator,
				seriesSpecification.getStartTimeStamp(), 
				seriesSpecification.getEndTimeStamp()
		);
		
		return new Iterable<TradeIndication>() {
			public Iterator<TradeIndication> iterator() {
				return wrappedIterator;
			}
		};
	}
	
	/**
	 * Determines the time interval of the candles. 
	 * Default value is 1 day.<br/>
	 * returns the associated timeFrame(TimeFrame)
	 * @return time frame.
	 */
	public TimeFrame getTimeFrame() { 
		return timeFrame;
	}

	/**
	 * Sets the candle time frame.<br/>
	 * sets the associated timeFrame(TimeFrame) with the given val(TimeFrame)
	 * @param val time frame.
	 */
	public void setTimeFrame(TimeFrame val) { 
		timeFrame = val;
	}
	
	/**
	 * Strategy that drives the conversion from candles to trade indications.<br/>
	 * returns the associated conversion(ICandleToTradeIndicationConversion)
	 * @return conversion strategy.
	 */
	public ICandleToTradeIndicationConversion getConversion() {
		return conversion;
	}
	
	/**
	 * Conversion strategy.<br/>
	 * sets the associated conversion(ICandleToTradeIndicationConversion) with the given val(ICandleToTradeIndicationConversion)
	 * @param val conversion strategy.
	 */
	public void setConversion(ICandleToTradeIndicationConversion val) {
		conversion = val;
	}
	
	/**
	 * Computes candle duration. For daily candles returns number of millis in a day,
	 * for 5-min candles returns number of millis in 5 minutes, etc.
	 * The computation is based on calendar, and arbitrarily (patrioticaly) 
	 * uses NY timezone and July 4, 2009 as a reference. Again, reference does not really affect
	 * the end result - just need to avoid proximity to "special" dates like when daytime light saving
	 * goes on/off.<br/>
	 * this basicaly returns the number of milliseconds in the given candleTimeFrame(TimeFrame)
	 */
	private static long inferMarginMillis(TimeFrame candleTimeFrame) {
		Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("America/New_York"));
		calendar.set(Calendar.YEAR, 2009);
		calendar.set(Calendar.MONTH, Calendar.JULY);
		calendar.set(Calendar.DAY_OF_MONTH, 4);
		calendar.set(Calendar.HOUR_OF_DAY, 0);
		calendar.set(Calendar.MINUTE, 0);
		calendar.set(Calendar.MILLISECOND, 0);
		
		long start = calendar.getTimeInMillis();
		
		candleTimeFrame.addToCalendar(calendar);
		
		long diff = calendar.getTimeInMillis() - start;
		
		return diff;
	}
}